package com.baichen.spring;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URL;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class ApplicationContext {

    private final Map<String, BeanDefinition> beanDefinitionMap = new HashMap<>();
    private final Map<String, Object> ioc = new HashMap<>();
    private final Map<String, Object> loadingIoc = new HashMap<>();
    private final List<BeanPostProcesser> beanPostProcessers = new ArrayList<>();

    ApplicationContext(final String packageName) throws Exception {
        initContext(packageName);
    }

    public void initContext(String packageName) throws Exception {
        scanPackage(packageName).stream().filter(this::canCreate).forEach(this::wrapper);
        initBeanPostProcesser();
        beanDefinitionMap.values().stream().forEach(this::createBean);
    }

    protected List<Class<?>> scanPackage(String packageName) throws Exception {
        List<Class<?>> classes = new ArrayList<Class<?>>();
        URL url = this.getClass().getClassLoader().getResource(packageName.replace(".", File.separator));
        Path path = Paths.get(url.toURI());

        Files.walkFileTree(path, new SimpleFileVisitor<Path>() {
            @Override
            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                Path absolutePath = file.toAbsolutePath();
                if (absolutePath.toString().endsWith(".class")) {
                    String replaceStr = absolutePath.toString().replace(".class", "").replace(File.separator, ".");
                    int indexOfPackageName = replaceStr.indexOf(packageName);
                    replaceStr = replaceStr.substring(indexOfPackageName);
                    try {
                        classes.add(Class.forName(replaceStr));
                    } catch (ClassNotFoundException e) {
                        throw new RuntimeException(e);
                    }
                }
                return FileVisitResult.CONTINUE;
            }
        });

        return classes;
    }

    protected boolean canCreate(Class<?> type) {
        return type.isAnnotationPresent(Component.class);
    }

    protected BeanDefinition wrapper(Class<?> type) {
        BeanDefinition beanDefinition = new BeanDefinition(type);
        if (beanDefinitionMap.containsKey(beanDefinition.getName())) {
            throw new RuntimeException("Duplicate bean name: " + type.getName());
        }
        beanDefinitionMap.put(beanDefinition.getName(), beanDefinition);
        return beanDefinition;
    }

    private void initBeanPostProcesser() {
        // createBean中已创建好的Processer会在后面创建的Processer在initializeBean中使用
        beanDefinitionMap.values().stream()
                .filter(bd -> BeanPostProcesser.class.isAssignableFrom(bd.getBeanType()))
                .map(this::createBean)
                .map(BeanPostProcesser.class::cast)
                .forEach(beanPostProcessers::add);
    }

    protected Object createBean(BeanDefinition beanDefinition) {
        String name = beanDefinition.getName();
        if (ioc.containsKey(name)) {
            return ioc.get(name);
        }
        if (loadingIoc.containsKey(name)) {
            return loadingIoc.get(name);
        }
        return doCreateBean(beanDefinition);
    }

    private Object doCreateBean(BeanDefinition beanDefinition) {
        Constructor<?> constructor = beanDefinition.getConstructorMethod();
        Object bean = null;

        try {
            bean = constructor.newInstance();
            loadingIoc.put(beanDefinition.getName(), bean);
            // 此处注入还是有问题，但是只有在使用了BeanPostProcesser将原来的对象替换了才会有问题：
            // 假设有循环依赖：A -> B -> A
            // 1. 创建A，原始A放入loadingIoc
            // 2. A注入B时，创建B
            // 3. B注入A时，从loadingIoc获取到原始A
            // 4. A完成依赖注入后，在initializeBean中被替换为代理A
            // 5. 结果：B中持有的是原始A，而不是代理A！
            autowiredBean(bean, beanDefinition);
            bean = initializeBean(bean, beanDefinition);
            loadingIoc.remove(beanDefinition.getName());
            ioc.put(beanDefinition.getName(), bean);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        return bean;
    }

    private void autowiredBean(Object bean, BeanDefinition beanDefinition) throws IllegalAccessException {
        for (Field field : beanDefinition.getAutowiredFields()) {
            field.setAccessible(true);
            Object autowiredBean = getBean(field.getType());

            if (autowiredBean != null) {
                field.set(bean, autowiredBean);
            } else {
                Autowired autowired = field.getAnnotation(Autowired.class);
                boolean required = autowired == null || autowired.required();
                if (required) {
                    throw new RuntimeException("autowired bean is null");
                }
            }
        }
    }

    private Object initializeBean(Object bean, BeanDefinition beanDefinition) throws IllegalAccessException, InvocationTargetException {
        for (BeanPostProcesser beanPostProcesser : beanPostProcessers) {
            bean = beanPostProcesser.beforeInitializeBean(bean, beanDefinition.getName());
        }

        Method postConstructMethod = beanDefinition.getPostConstructMethod();
        if (postConstructMethod != null) {
            postConstructMethod.invoke(bean);
        }

        for (BeanPostProcesser beanPostProcesser : beanPostProcessers) {
            bean = beanPostProcesser.afterInitializeBean(bean, beanDefinition.getName());
        }

        return bean;
    }

    public Object getBean(String beanName) {
        if (beanName == null) {
            return null;
        }

        Object bean = ioc.get(beanName);
        if (bean != null) {
            return bean;
        }

        if (beanDefinitionMap.containsKey(beanName)) {
            return createBean(beanDefinitionMap.get(beanName));
        }

        return null;
    }

    public <T> T getBean(Class<T> beanType) {
        String beanName = beanDefinitionMap.values().stream()
                .filter(bd -> beanType.isAssignableFrom(bd.getBeanType()))
                .map(BeanDefinition::getName)
                .findFirst()
                .orElse(null);

        return (T) getBean(beanName);
    }

    public <T> List<T> getBeans(Class<T> beanType) {
        return beanDefinitionMap.values().stream()
                .filter(bd -> bd.getBeanType().isAssignableFrom(beanType))
                .map(BeanDefinition::getName)
                .map(this::getBean)
                .map(b -> (T) b)
                .collect(Collectors.toList());
    }
}
